6.11. Общее о проектировании и архитектуре
Общее о проектировании и архитектуре программного обеспечения
Программное обеспечение — это не произвольная последовательность инструкций, исполняемая компьютером. Это система, обладающая структурой, логикой внутренней организации и связями с внешним окружением. Вне зависимости от масштаба (одностраничный веб-интерфейс, мобильное приложение, распределённая корпоративная платформа), ПО существует в рамках ограничений: функциональных требований, ресурсов, времени, качества, сопровождаемости. Именно архитектура и проектирование формируют основу, позволяющую удерживать баланс между этими ограничениями.
Почему так важно проектирование ПО
Во многих практиках разработка начинается немедленно после получения первоначального описания задачи — без предварительного анализа, без моделирования, без согласования границ ответственности между компонентами. Причина, как правило, одна: дефицит времени. Однако такая экономия времени на начальной стадии неизбежно приводит к многократно возрастающим затратам на последующих этапах — при расширении функциональности, интеграции, исправлении дефектов, миграции на новую инфраструктуру или даже при обычном сопровождении.
Проектирование — это не формальная процедура и не бюрократический этап. Это интеллектуальная деятельность, направленная на выработку структуры, которая:
- делает систему понятной для команды разработчиков;
- предотвращает распространение ошибок за пределы локальных зон ответственности;
- позволяет разделять усилия: разные группы могут работать над разными частями одновременно, не нарушая целостности;
- обеспечивает способность к адаптации — когда требования изменяются, изменения затрагивают минимально возможное число элементов;
- снижает риски — архитектурные просчёты, выявленные на ранней стадии, стоят в разы дешевле, чем аналогичные, обнаруженные в продакшене.
Ключевое заблуждение состоит в том, что проектирование — это только чертежи и схемы. На самом деле, проектирование начинается с осмысления домена, с формулировки того, какие сущности, события и правила являются существенными для предметной области, какие взаимодействия являются постоянными, а какие — временными, какие зависимости допустимы, а какие — критичны. Без этого осмысления любая схема остаётся декорацией, а не инструментом принятия решений.
Архитектура как стратегический уровень проектирования
Архитектура программного обеспечения — это совокупность стратегических решений, определяющих основные структурные элементы системы, их взаимосвязи, а также принципы, по которым эти элементы взаимодействуют с внешним окружением. Архитектура отвечает на вопросы: «Как будет организовано всё в целом?», «Какие части смогут меняться независимо?», «Какие технологии зададут пределы масштабируемости?», «Где будут находиться границы ответственности?».
Архитектура — живой процесс принятия решений, который продолжается на протяжении всего жизненного цикла проекта. Однако первоначальные архитектурные решения обладают наибольшей инерцией: изменить их позже — это перестройка. Поэтому ошибки на этом уровне особенно дорогостоящи.
Отсюда следует, что роль архитектора ПО — формулировать и отстаивать структурные ограничения, в рамках которых возможна дальнейшая разработка. Архитектор должен уметь:
- видеть систему как единое целое, а не как набор отдельных модулей;
- понимать технические возможности инструментов и их долгосрочные последствия — в плане сопровождаемости, безопасности, производительности, затрат на инфраструктуру;
- идентифицировать слабые места в предлагаемых решениях до того, как они станут проблемами;
- оценивать компромиссы между скоростью разработки, качеством, масштабируемостью и стоимостью владения.
Этот навык вырабатывается в ходе столкновения с ограничениями, с техническим долгом, с инцидентами, вызванными архитектурными просчётами. Опыт «горящих» проектов, где приходится принимать решения в условиях дефицита времени и ресурсов, зачастую даёт гораздо более глубокое понимание системных взаимосвязей, чем несколько лет стабильного развития. Именно поэтому профессиональный архитектор — это человек с большим стажем, прошедший через необходимость реструктуризации, рефакторинга, миграции, восстановления.
Системность
В практике часто под «системой» подразумевают отдельное приложение: веб-сайт, десктопную программу, мобильное приложение. Однако в реальных условиях, особенно в корпоративной и государственной среде, информационная система — это совокупность взаимосвязанных приложений, сервисов, хранилищ и внешних интеграций, которые вместе реализуют бизнес-процессы. Она не сводится к единому исполнимому файлу или даже к единому кодовому репозиторию.
Например, в системе электронного документооборота может быть:
- модуль ввода заявок — реализован на Java и работает в контейнере;
- модуль согласования — написан на C# и развёрнут как Windows-служба;
- модуль хранения скан-копий — отдельное приложение на Python, интегрирующееся с файловым хранилищем;
- модуль уведомлений — микросервис на Node.js, отправляющий сообщения в Telegram и почту;
- внешние интеграции: ЕСИА, ГИС ЖКХ, реестр юридических лиц — каждый со своим протоколом и форматом.
Все эти элементы принадлежат одной логической системе, несмотря на различия в стеках, окружениях и жизненных циклах. Архитектура ПО в таком случае — это архитектура взаимодействия между компонентами. Именно здесь проявляется необходимость в чётких договорённостях: какие интерфейсы считаются стабильными, какие форматы данных обязательны, какие гарантии доставки требуются, как обрабатываются ошибки на стыке систем.
Это означает, что архитектурные решения должны учитывать внутреннюю структуру компонентов и их интерфейсы с внешним миром. Порт — это договорённость о контракте: какие данные ожидаются, в каком виде, с какими ограничениями, с какой семантикой. Нарушение этого контракта — нарушение целостности всей системы.
Уровни детализации
Часто термины архитектура и дизайн (технический дизайн, проектирование) используются как синонимы. Однако они относятся к разным уровням абстракции и разным целям.
Архитектура — это то, что трудно изменить позже без значительных затрат. Это выбор парадигмы (монолит, микросервисы), распределение ответственности между основными подсистемами, принятие решений о размещении данных (единая база, шардирование, полиглотное хранение), определение стратегии развертывания (одна среда, multi-tenant, multi-region), выбор протоколов синхронизации, политика управления зависимостями, границы контекстов (в терминах DDD), уровень изоляции компонентов.
Дизайн (техническое проектирование) — это то, как реализуется конкретный компонент внутри заданных архитектурных рамок. Это выбор паттернов проектирования (Factory, Strategy, Observer), организация классов и интерфейсов, структура базы данных (нормализация, денормализация), алгоритмы обработки, обработка исключений, логирование. Дизайн может меняться относительно часто, поскольку он оперирует в рамках уже утверждённой архитектурной модели.
Можно сказать, что архитектура определяет «каркас», а дизайн — «наполнение». Каркас должен быть прочным и гибким одновременно: он не должен ломаться под нагрузкой, но и не должен мешать строить внутри него новые помещения. Технический дизайн, в свою очередь, должен использовать возможности каркаса, а не пытаться его переопределить.
Уровни архитектурного рассмотрения
Понимание архитектуры требует системного взгляда. Для этого удобно выделять четыре уровня, каждый из которых отвечает на свой круг вопросов, формируя полную картину:
1. Функциональная архитектура
Этот уровень описывает, что система делает с точки зрения конечного пользователя или внешней системы. Здесь фиксируются бизнес-возможности: регистрация, авторизация, оплата, поиск, генерация отчётов. Акцент делается на сценариях использования: «пользователь оформляет заказ», «оператор подтверждает оплату», «администратор настраивает тарифы».
Функциональная архитектура — это мост между бизнес-аналитикой и технической реализацией. Она помогает избежать ситуации, когда разработчики идеально реализуют техническое задание, но полученный результат не решает реальную бизнес-задачу. На этом уровне строятся use-case диаграммы, пользовательские истории, потоки действий — всё, что позволяет убедиться: мы строим ту систему, которая действительно нужна.
2. Архитектура предметной области (домена)
Если функциональная архитектура говорит что, то архитектура домена говорит почему и как с точки зрения бизнес-логики. Здесь выделяются ключевые сущности: Пользователь, Заказ, Счёт, Транзакция; определяются бизнес-правила: «нельзя отменить платёж после подтверждения банком», «скидка применяется только к товарам категории А», «статус заявки меняется только при наличии подписи уполномоченного лица»; фиксируются неизменяемые события: «Заказ создан», «Платёж проведён», «Документ согласован».
Этот уровень не зависит от технологий. Он может быть описан в терминах UML-диаграмм классов, Event Storming-сессий, онтологий или просто текстом — главное, чтобы отражалась семантика предметной области. Архитектура домена — это основа для DDD (Domain-Driven Design), и она позволяет выстраивать границы агрегатов, выделять ограниченные контексты и тем самым минимизировать межкомпонентные зависимости.
3. Прикладная архитектура
Здесь начинается переход от абстракций к реализации. Прикладная архитектура отвечает на вопрос: как реализованы функции и правила? Это уровень компонентов, модулей, сервисов, API, протоколов взаимодействия. Здесь определяется, как компоненты передают друг другу данные: через REST, gRPC, сообщения в очереди, shared memory; как организованы границы: по функциональным зонам («платежи», «доставка»), по типам операций (CRUD-сервисы, вычислительные ядра), по уровням доступа (публичный API, внутренние сервисы, админка).
Прикладная архитектура — это уровень, где материализуются архитектурные стили (слоистая, гексагональная, чистая архитектура). Именно здесь проявляется, насколько удачно были выделены границы ответственности: если изменение в одном модуле требует правок в десяти других — границы проведены некорректно.
4. Технологическая архитектура
Этот уровень отвечает за инфраструктурную реализацию: где и как размещаются компоненты, какие ресурсы для них выделяются, как обеспечивается отказоустойчивость, масштабируемость, безопасность. Сюда входят: выбор облачной платформы (AWS, Azure, Yandex Cloud) или on-premise-развёртывания, типы баз данных (реляционные, документные, временные ряды), использование контейнеризации (Docker, Kubernetes), настройка сетей (VPC, зоны доступности), системы мониторинга и логирования (Prometheus, ELK), стратегии резервного копирования.
Технологическая архитектура — это логика размещения и взаимодействия ресурсов. Например, решение разнести компоненты по разным availability zones — это архитектурное решение, направленное на достижение отказоустойчивости. Использование read-replicas для снятия нагрузки с основной базы — тоже архитектурное решение. Оно должно приниматься осознанно, а не как побочный эффект от «просто поставили Kubernetes».
Изменения на одном уровне неизбежно влияют на другие. Например, решение перейти на микросервисы (прикладная архитектура) потребует пересмотра стратегии деплоя (технологическая), может повлиять на выделение границ домена (архитектура домена) и даже изменить пользовательский сценарий (функциональная — если появляются асинхронные уведомления о статусе).
Виды архитектур
Архитектурные решения, касающиеся распределения компонентов и организации их взаимодействия, определяют фундаментальные свойства системы: её масштабируемость, отказоустойчивость, сложность сопровождения и стоимость владения. Выбор между локальной, файл-серверной, клиент-серверной, SOA или микросервисной архитектурой — это выбор стратегии управления зависимостями и согласованностью.
Локальные системы
Локальная архитектура — это размещение всей логики и данных на одном устройстве, без необходимости сетевого взаимодействия. Примеры: текстовый редактор на настольном компьютере, оффлайн-игра на смартфоне, приложение для ведения заметок без синхронизации.
Важно понимать, что локальность не означает «простоту». Локальные приложения могут быть сложными: содержать собственные базы данных (SQLite), реализовывать сложную логику, поддерживать плагины. Однако их ключевое свойство — отсутствие внешних временных зависимостей. Система работает до тех пор, пока работает само устройство. Нет проблем с сетевыми задержками, таймаутами, частичными отказами, несогласованными состояниями.
Это делает локальную архитектуру идеальной там, где важна предсказуемость и автономность: медицинские устройства, встроенные системы, инструменты для анализа данных на персональной станции, оффлайн-доступ к критическим документам. Однако она не масштабируется по пользователям и не поддерживает совместную работу без внешнего механизма синхронизации — а добавление такого механизма уже выводит систему за пределы локальной архитектуры.
Файл-серверная архитектура
Файл-серверная архитектура возникает, когда несколько клиентов получают доступ к одним и тем же файлам через сетевой ресурс (например, SMB-шара, NFS-точка). Сервер здесь — пассивный посредник: он не интерпретирует содержимое файлов, не управляет целостностью данных, не реализует логику. Он лишь предоставляет механизм хранения и передачи блоков данных.
На первый взгляд — простое и дешёвое решение. На практике — источник глубоких системных проблем.
Главная трудность — согласованность разделяемого состояния. Когда два клиента одновременно читают и изменяют один и тот же файл, возникает риск конфликта: кто перезапишет чьи изменения? Кто увидит актуальное состояние? Клиенты вынуждены самостоятельно реализовывать блокировки (file locks), проверки версий, стратегии разрешения конфликтов — что редко делается корректно. В результате — повреждение данных, потеря транзакций, нестабильность.
Другая проблема — отсутствие семантики. Сервер не знает, что представляет собой файл: база данных, конфигурация, документ. Поэтому он не может применять оптимизации (кеширование, индексация, репликация), не может обеспечить безопасность на уровне операций («запретить удаление, но разрешить чтение»), не может логировать смысловые действия («пользователь изменил заказ №123»), а только технические («записан блок 0x4A в файл orders.db»).
Файл-серверная архитектура оправдана только в узких сценариях: раздача статических ресурсов (изображений, PDF-документов), совместное использование read-only-конфигураций, временные рабочие каталоги. Для любых динамических данных она — технический долг с высокой процентной ставкой.
Клиент-серверная архитектура
Клиент-серверная архитектура — это переход от пассивного хранения к активной обработке на стороне сервера. Здесь сервер — полноценный участник взаимодействия: он интерпретирует запросы, применяет бизнес-логику, управляет состоянием, обеспечивает целостность данных.
Это фундаментальная смена парадигмы: вместо того чтобы доверять клиентам корректную работу с данными, сервер берёт на себя ответственность за согласованность. Клиент становится представлением, сервер — источником истины.
В пределах клиент-серверной архитектуры выделяют несколько стратегических подходов — монолит, SOA, микросервисы. Их отличает техническая реализация и логика декомпозиции ответственности.
Монолит
Монолит — это единое приложение, в рамках которого реализованы все функции: обработка запросов, бизнес-логика, доступ к данным, иногда даже часть интерфейса (в случае SSR). Всё развёрнуто как один процесс, использует одну базу данных (или один кластер), имеет единый жизненный цикл.
Важно избегать стигматизации термина «монолит». Монолит не означает «плохой код» или «неподдерживаемая система». Это — архитектурная стратегия, имеющая чёткие границы применимости.
Преимущества монолита проявляются в следующих условиях:
- предметная область сравнительно узкая и стабильна;
- команда небольшая, и все участники могут оперировать единым контекстом;
- важна низкая задержка между операциями (все вызовы — внутрипроцессные);
- нет жёстких требований к независимому масштабированию отдельных функций;
- критична простота развёртывания и отладки.
В таких случаях монолит минимизирует накладные расходы: нет сетевых вызовов между компонентами, нет проблем с распределёнными транзакциями, нет необходимости в сложных механизмах согласования данных. Изменение затрагивает одну кодовую базу, один CI/CD-конвейер, одну среду развёртывания.
Однако при росте сложности, при расширении команды, при появлении требований к масштабируемости по отдельным функциям, монолит начинает демонстрировать системные ограничения:
- увеличивается время сборки и тестирования;
- возрастает вероятность регрессий: изменение в одном модуле может повлиять на другой, даже если связь неочевидна;
- становится трудно внедрять новые технологии: вся система привязана к одному стеку;
- масштабирование возможно только по всей системе целиком, даже если нагрузка растёт только в одном сценарии;
- время запуска и потребление памяти растут, что влияет на плотность развёртывания.
Эти ограничения не делают монолит «неправильным» — они делают его неоптимальным для конкретного контекста. Архитектурное решение должно соответствовать масштабу и динамике проекта. Стартап с MVP вряд ли выиграет от микросервисов; крупная ERP-система с десятками тысяч пользователей — от монолита.
Двухзвенная и трёхзвенная архитектура
Часто монолитную архитектуру путают с отсутствием внутренней структуры. Это ошибка. Даже в рамках единого процесса возможно и необходимо разделение на зоны ответственности.
Двухзвенная (two-tier) архитектура — это прямое взаимодействие клиента (например, толстого клиента на C# или JavaFX) с базой данных. Логика частично размещена на клиенте (валидация, форматирование), частично — в хранимых процедурах или триггерах. Такая архитектура типична для десктопных приложений 1990–2000-х.
Проблемы: высокая связанность клиента и схемы БД, сложность обновления (требуется развёртывание на всех клиентах), отсутствие централизованного контроля над логикой, уязвимость к SQL-инъекциям при некорректной реализации.
Трёхзвенная (three-tier) архитектура — это классическое разделение на:
- Презентационный уровень — клиент (веб-браузер, мобильное приложение, SPA);
- Уровень приложения (бизнес-логики) — серверное приложение, обрабатывающее запросы, реализующее правила, управляющее транзакциями;
- Уровень данных — СУБД, файловое хранилище, кэш.
Это — наиболее распространённая форма монолита в вебе. Всё может быть одной программой (например, ASP.NET MVC с встроенным контроллером и сервисами), но внутренняя декомпозиция уже присутствует. Такая структура позволяет:
- изолировать интерфейс от логики — изменения в дизайне не влияют на ядро;
- централизовать логику — все правила обрабатываются в одном месте;
- упростить безопасность — клиент не имеет прямого доступа к данным;
- обеспечить масштабируемость по запросам — можно добавлять экземпляры приложения перед балансировщиком.
Трёхзвенная архитектура — логическая декомпозиция внутри единого контекста выполнения. Это важный промежуточный шаг, позволяющий сохранить преимущества монолита, одновременно снижая связанность.
SOA (Service-Oriented Architecture)
SOA возникает, когда монолит становится слишком большим, и появляется потребность в независимой эволюции частей системы. В отличие от микросервисов, SOA предполагает централизованное управление взаимодействием через шину (ESB — Enterprise Service Bus).
Каждый сервис в SOA — это логически обособленная функция: «управление пользователями», «обработка платежей», «формирование отчётов». Сервисы общаются через ESB, который:
- маршрутизирует запросы;
- преобразует форматы (XML ↔ JSON, старая версия ↔ новая);
- обеспечивает безопасность (аутентификация, авторизация);
- управляет транзакциями (в пределах возможностей протоколов);
- логирует взаимодействия.
ESB — это архитектурный компонент ответственности. Он берёт на себя часть сложности распределённых систем, изолируя её от сервисов. Это позволяет сервисам оставаться относительно простыми — они не думают о том, кто их вызывает, в каком формате пришёл запрос, нужно ли его преобразовать.
Однако централизация имеет цену:
- ESB становится единым узлом отказа: его остановка парализует всю систему;
- он сам требует высокой квалификации для настройки и сопровождения;
- преобразования и маршрутизация добавляют задержку;
- изменение контрактов требует координации с ESB.
SOA оправдана в средах с высокой регламентацией, где требуется строгий контроль над интеграциями: банки, госструктуры, крупные промышленные предприятия. Здесь важна аудируемость, стандартизация, соответствие нормативам — и ESB предоставляет инструменты для этого.
Микросервисы
Микросервисная архитектура — это стратегия, основанная на максимальной автономии компонентов:
- каждый микросервис — независимое приложение со своим жизненным циклом;
- он владеет своей базой данных (или, как минимум, своей схемой в общей БД, но без прямого доступа к данным других сервисов);
- взаимодействие — через явные контракты (API, события), без централизованного посредника;
- масштабирование, развёртывание, мониторинг — независимы.
Ключевой принцип — граница контекста как граница ответственности. Микросервис не только реализует функцию — он инкапсулирует весь стек, необходимый для её выполнения: код, данные, зависимости. Это позволяет командам работать автономно: одна команда отвечает за «платежи», другая — за «доставку», и они могут выпускать обновления независимо, при условии соблюдения контрактов.
Однако такая автономия требует компенсации в других местах. Если в монолите транзакция охватывала несколько операций «из коробки», то в микросервисах приходится реализовывать саги — последовательности компенсирующих действий. Если в монолите отладка шла через breakpoint, то в распределённой системе требуется централизованная трассировка (например, через OpenTelemetry). Если в монолите балансировка нагрузки решалась на уровне приложения, то в микросервисах появляются sidecar-прокси (Envoy), service mesh (Istio), discovery-сервисы (Consul).
Микросервисы — средство. Они оправданы, когда:
- система настолько велика, что одна команда не может эффективно управлять всей кодовой базой;
- разные части системы имеют разную динамику изменений (например, маркетинговый сайт меняется ежедневно, а ядро расчётов — раз в квартал);
- требуются разные стратегии масштабирования (высокочастотные API vs вычислительные задачи);
- допустимы асинхронные взаимодействия и eventual consistency.
Во всех остальных случаях микросервисы вносят избыточную сложность без адекватной отдачи.
Стили внутренней организации
Выбор архитектурного стиля — это выбор стратегии направления зависимостей. В любой системе возникают зависимости: между классами, модулями, слоями, внешними системами. Ключевой вопрос в том, как сделать зависимости управляемыми. Хороший стиль минимизирует зависимость стабильных, критичных частей системы (бизнес-логики, доменных правил) от нестабильных (интерфейсов, инфраструктуры, внешних API).
Это достигается через инверсию зависимостей — принцип, согласно которому высокоуровневые модули не должны зависеть от низкоуровневых; оба должны зависеть от абстракций. Архитектурные стили — это конкретные реализации этого принципа на уровне приложения.
Слоистая архитектура (Layered Architecture)
Слоистая архитектура — наиболее распространённый и интуитивно понятный стиль. Система разделяется на горизонтальные слои, каждый из которых имеет чёткую зону ответственности, и зависимости допускаются только в одном направлении — от верхних слоёв к нижним.
Типичная трёхслойная структура:
-
Презентационный слой (Presentation) — отвечает за взаимодействие с пользователем или внешней системой: HTTP-контроллеры, GraphQL-ресолверы, UI-компоненты (в случае SSR). Здесь формируются запросы и отдаются ответы. Логика здесь минимальна: преобразование данных, валидация входных параметров, маршрутизация.
-
Слой приложения (Application / Business Logic) — сердце системы. Здесь реализуются сценарии использования: «оформить заказ», «отменить бронирование», «рассчитать налог». Этот слой координирует работу доменных сущностей, транзакций, внешних вызовов. Он не содержит деталей реализации (как именно сохраняются данные, как отправляется email), но знает, когда и в каком порядке это должно происходить.
-
Слой инфраструктуры (Infrastructure / Persistence) — техническая поддержка: доступ к базе данных, работа с внешними API, отправка сообщений, логирование, кэширование. Здесь живут репозитории, HTTP-клиенты, ORM-маппинги.
Ключевой приём — зависимости направлены вниз, но реализация низкоуровневых компонентов внедряется вверх через интерфейсы (Dependency Injection). Например, слой приложения зависит не от конкретного SqlOrderRepository, а от интерфейса IOrderRepository. Конкретная реализация подаётся из слоя инфраструктуры при старте приложения.
Такая структура обеспечивает:
- чёткое разделение ответственности — легко понять, где искать нужный код;
- возможность тестирования слоя приложения в изоляции (mock-реализации репозиториев);
- заменяемость инфраструктурных компонентов без перекомпиляции ядра.
Однако у слоистой архитектуры есть ограничение: она хорошо масштабируется по функциональности, но плохо — по доменным контекстам. Когда система охватывает несколько независимых предметных областей (например, «продажи», «склад», «финансы»), слои начинают «размазываться»: в одном слое приложения смешиваются правила из разных доменов, в слое инфраструктуры — репозитории, не связанные логически. Это приводит к росту связанности и снижению возможности автономной эволюции.
Гексагональная архитектура (Ports & Adapters)
Гексагональная архитектура — ответ на проблему внешних зависимостей. Её цель — сделать ядро приложения полностью независимым от того, откуда приходят данные и куда они уходят. Ядро («гексагон») содержит только бизнес-логику и оперирует через порты — интерфейсы, определяющие, что может делать система (например, IUserRepository, INotificationService, IPaymentGateway).
Внешние адаптеры — это реализации этих портов:
- Первичные (driving) адаптеры — инициируют вызовы в ядро: HTTP-контроллеры, CLI-команды, scheduled-задачи. Они преобразуют внешний запрос в вызов порта.
- Вторичные (driven) адаптеры — реагируют на вызовы из ядра: реализации репозиториев, HTTP-клиенты для внешних систем, отправка email через SMTP.
Суть в том, что ядро ничего не знает о вебе, базах данных, протоколах. Оно зависит только от своих портов — абстракций, определённых внутри ядра. Адаптеры зависят и от портов, и от внешних технологий. Направление зависимостей — внутрь гексагона, а не наружу.
Преимущества:
- возможность легко менять способ взаимодействия: заменить REST на gRPC — достаточно написать новый первичный адаптер;
- тестирование ядра в полной изоляции — все зависимости заменяются на заглушки;
- чёткое выделение контрактов: порт — это публичный API ядра, его стабильность критична.
Гексагональная архитектура особенно эффективна в системах с множеством интеграций, где внешние интерфейсы часто меняются (например, платёжные шлюзы, CRM, ERP), а бизнес-логика остаётся стабильной. Она также хорошо сочетается с DDD: порты часто соответствуют границам агрегатов или ограниченных контекстов.
Чистая архитектура (Clean Architecture)
Чистая архитектура — развитие идей гексагональной, с акцентом на иерархию стабильности. Она формализует круги зависимости:
-
Entities (Сущности) — ядро ядра. Бизнес-объекты, инкапсулирующие критически важные правила, не зависящие ни от чего внешнего:
Order,Account,Policy. Могут быть чистыми классами или даже просто структурами данных с методами. Эти правила должны выживать даже при полной смене технологий. -
Use Cases (Сценарии использования) — оркестрация: как сущности взаимодействуют для выполнения бизнес-задачи. Например,
PlaceOrderUseCaseполучает данные, создаётOrder, проверяет инвентарь, инициирует платёж. Зависит от Entities, но не от внешних технологий. -
Interface Adapters (Адаптеры интерфейсов) — преобразование данных между форматами: контроллеры, презентеры, репозитории-обёртки, DTO-мапперы. Здесь происходит адаптация к конкретному фреймворку (MVC, gRPC), но без логики.
-
Frameworks & Drivers (Фреймворки и драйверы) — внешние зависимости: базы данных, веб-серверы, UI-фреймворки, внешние API. Эта зона наиболее нестабильна.
Зависимости направлены от внешних кругов к внутренним. Entities ничего не знают о Use Cases; Use Cases не знают о том, как именно реализованы репозитории. Это достигается через интерфейсы и DI.
Чистая архитектура — не требование использовать определённые папки или файлы. Это принцип стабильности: чем ближе к центру, тем дольше живёт код. Сущности могут оставаться неизменными годами; адаптеры — меняться с новой версией фреймворка.
Этот стиль оправдан там, где долгосрочная поддержка важнее скорости первоначальной разработки: госзаказ, медицинские системы, финансовые ядра. Он снижает стоимость владения, но требует дисциплины — легко «протечь» зависимость из внешнего круга во внутренний (например, добавить атрибут Id типа Guid в доменную сущность только потому, что так требует ORM).
Событийно-ориентированная архитектура (Event-Driven Architecture)
Событийно-ориентированная архитектура — это смещение акцента с запрос-ответ на публикация-подписка. Вместо того чтобы компоненты вызывали друг друга напрямую, они обмениваются событиями — неизменяемыми фактами о том, что произошло: OrderPlaced, PaymentConfirmed, InventoryUpdated.
Ключевые элементы:
- Источник события — компонент, фиксирующий факт и публикующий событие в шину (Kafka, RabbitMQ, AWS SNS/SQS);
- Шина сообщений — инфраструктурный компонент, обеспечивающий доставку;
- Подписчики — компоненты, реагирующие на события и выполняющие свои задачи (обновление склада, отправка уведомления, аналитика).
Событие — не команда. Оно не говорит «сделай», а констатирует «сделано». Это позволяет строить асинхронные, слабосвязанные системы.
Преимущества:
- масштабируемость: обработчики событий можно масштабировать независимо;
- отказоустойчивость: если один обработчик упал, событие остаётся в очереди;
- расширяемость: новые функции можно добавлять просто подпиской на существующие события;
- историчность: события можно сохранять как журнал (event log), что даёт полную аудиторскую трассу.
Однако события вносят новую сложность:
- eventual consistency — состояние системы может быть временно несогласованным;
- отладка требует инструментов трассировки по цепочке событий;
- дублирование событий или их потеря требуют идемпотентных обработчиков;
- проектирование событий — нетривиальная задача: слишком грубое событие (например,
OrderUpdated) несёт мало информации; слишком детальное — жёстко связывает издателя и подписчика.
Событийно-ориентированная архитектура особенно эффективна в системах с высокой нагрузкой, где важна асинхронность: электронная коммерция, логистика, финтех, IoT. Часто она используется как дополнение к другим: например, в чистой архитектуре сценарий использования после выполнения публикует событие, а инфраструктурный адаптер его отправляет.
Компонентно-ориентированная архитектура (Component-Based Architecture)
Этот стиль чаще применяется на уровне пользовательского интерфейса, но принципы переносятся и на бэкенд. Основная идея — разбиение системы на повторно используемые, самодостаточные компоненты, каждый из которых инкапсулирует:
- состояние;
- поведение;
- представление (если применимо);
- интерфейс взаимодействия с другими компонентами.
Компонент — это не просто класс или модуль. Это контракт: что он принимает на вход, что выдаёт на выход, какие события генерирует, какие зависимости требует. В вебе это — React-компоненты, Angular-модули, Web Components. В бэкенде — библиотеки с чётким API, микросервисы с открытой спецификацией (OpenAPI), плагины с фиксированным интерфейсом.
Компонентно-ориентированный подход позволяет:
- собирать сложные интерфейсы из простых блоков;
- переиспользовать функционал без дублирования;
- изолировать изменения: если компонент меняет внутреннюю реализацию, но сохраняет контракт — внешний код не требует правок.
Ключевое условие успеха — строгая дисциплина контрактов. Компонент без документированного API быстро превращается в «чёрный ящик», зависимость от которого становится техническим долгом.
Проектирование как процесс
Проектирование — это не единовременное действие, совершаемое «в начале проекта», и не обязанность одного человека, «архитектора». Это непрерывный цикл выявления требований, формирования гипотез, проверки их на практике и корректировки структуры. Оно включает техническую, коммуникационную и документационную составляющие.
Почему схемы важны
Схемы — это не иллюстрации к тексту. Это инструмент мышления, позволяющий выйти за пределы линейного описания и увидеть систему как целое. Текст хорошо передаёт последовательности и условия; схема — структуру, связи, иерархию, потоки.
Три ключевые функции схем:
-
Декомпозиция сложности. Система в целом может быть непостижима. Схема позволяет выделить части (компоненты, зоны, контексты), ограничить внимание и рассмотреть каждую отдельно, не теряя связи с общим контекстом.
-
Выявление скрытых зависимостей. В текстовом описании легко упустить косвенную связь: «модуль A вызывает модуль B, который сохраняет данные, которые потом читает модуль C». На диаграмме такая цепочка становится видимой. Это позволяет обнаружить циклические зависимости, узкие места, точки единого отказа до реализации.
-
Согласование понимания в команде. Когда несколько человек смотрят на одну и ту же схему, они обсуждают конкретные блоки и стрелки. Это снижает вероятность недопонимания: «под сервисом X я имел в виду именно этот блок, а не тот». Схема — это общий язык, особенно важный при участии нетехнических специалистов (аналитики, заказчики).
Схема должна быть достаточно точной, но не перегруженной. Диаграмма уровня 0 (системный контекст) показывает только внешние системы и основные потоки. Диаграмма уровня 1 (прикладная архитектура) — компоненты и их интерфейсы. Диаграмма уровня 2 — внутреннее устройство одного компонента. Каждый уровень служит своей цели и своей аудитории. Перемешивание уровней (например, на общей схеме рисовать сервисы и классы внутри них) делает схему бесполезной.
Рекомендуемые типы схем для проектирования:
- C4 Model (Context, Containers, Components, Code) — иерархическая система диаграмм, позволяющая описывать систему на четырёх уровнях детализации. Особенно эффективна для документирования и передачи знаний.
- Схемы потоков данных (DFD) — фокус на том, как данные перемещаются между процессами, хранилищами и внешними сущностями. Полезны на ранних стадиях, когда ещё не решено, какие будут компоненты.
- UML-диаграммы (Component, Deployment, Sequence) — при условии их умеренного использования. Диаграммы компонентов и развёртывания помогают прояснить физическую и логическую структуру; sequence-диаграммы — уточнить взаимодействие в критических сценариях.
- Event Storming-карты — совместное моделирование домена через выделение событий, команд, агрегатов. Это результат workshop’а, фиксирующий коллективное понимание.
Схемы теряют ценность, если они:
- создаются «для галочки» и не обсуждаются;
- не обновляются при изменении архитектуры;
- слишком абстрактны («Бизнес-логика» → «Хранилище») или, наоборот, переполнены деталями.
Идеальная схема — та, которую можно распечатать, повесить на стену и использовать как опору для разговора.
Планирование
Планирование проектирования начинается с уточнения ограничений и критериев качества. Нефункциональные требования (NFR) — это не «желательно быстро работать», а:
- «Система должна обрабатывать до 5000 запросов в секунду с 95-м перцентилем задержки ≤ 200 мс»;
- «Данные должны быть доступны с гарантией 99.99% uptime в течение года»;
- «Изменение налоговой ставки должно вступать в силу в течение 5 минут без перезапуска»;
- «Нарушение безопасности одного компонента не должно давать доступ к данным пользователей в других».
Эти требования напрямую влияют на архитектурные решения. Например:
- требование низкой задержки может исключить синхронные вызовы между микросервисами;
- требование мгновенного обновления правил может сделать невозможным хардкодинг логики в бинарниках;
- требование изоляции — повлечь необходимость в multi-tenancy на уровне БД или даже отдельных инстансах.
Планирование включает:
- выявление ключевых сценариев (пиковая нагрузка, восстановление после сбоя, развёртывание новой функции);
- оценку рисков (что сломается первым? где будут узкие места?);
- выбор критериев для сравнения вариантов (стоимость, время вывода на рынок, гибкость, обучаемость);
- определение границ экспериментов: что можно проверить быстро (например, через spike-реализацию), что требует прототипа, а что — фундаментального решения.
Важно: планирование не даёт «правильного» ответа. Оно помогает обосновать выбор и зафиксировать, почему принято то или иное решение — особенно если позже возникнет необходимость его пересмотреть.
Архитектурные решения
Каждое существенное архитектурное решение должно быть зафиксировано в виде документа, доступного всей команде. Для этого используются ADR (Architecture Decision Records) — лёгкие, структурированные записи вида:
- Контекст: какая проблема или ограничение требует решения?
- Варианты: какие альтернативы рассматривались?
- Решение: что выбрано и почему?
- Последствия: какие преимущества и недостатки несёт это решение? какие компромиссы сделаны?
Пример:
Контекст: Необходимо реализовать интеграцию с 5 платёжными системами, каждая из которых меняет API раз в 6–12 месяцев.
Варианты:
- Жёстко закодировать логику в одном модуле.
- Выделить общий интерфейс и реализовать адаптеры для каждой системы.
- Использовать внешний оркестратор (например, Apache Camel).
Решение: Вариант 2 — гексагональный подход с портомIPaymentGateway.
Последствия:
- Новые платёжные системы подключаются без изменения ядра.
- Тестирование проще (можно мокать адаптеры).
– Требуется больше начальных усилий на выделение интерфейса.
ADR — это история рассуждений, позволяющая новым членам команды понять, почему система устроена именно так, и избежать повторного обсуждения уже решённых вопросов. Со временем ADR образуют карту архитектурной эволюции проекта.
Командная работа
Даже в небольшой команде важно вовлекать разработчиков в проектирование для выявления скрытых проблем. Тот, кто будет писать код, часто видит практические сложности раньше, чем теоретик.
Эффективные практики:
- Architecture Kata — совместное решение архитектурной задачи за ограниченное время. Задаётся контекст («нужно построить систему бронирования для 10 млн пользователей»), и команда за 60–90 минут вырабатывает вариант архитектуры, рисует схему, оценивает риски. Это тренирует системное мышление.
- Lightweight Architecture Decision Sessions — короткие встречи (30–45 мин) для принятия одного конкретного решения с участием ключевых участников (разработчик, техлид, аналитик, инфраструктурщик). Фокус — на аргументах, а не на голосовании.
- Architecture Review on Pull Requests — код-ревью и проверка, не нарушает ли изменение архитектурные границы, не вносит ли новые циклические зависимости, не ломает ли контракты.
Проектирование — это инвестиция в предсказуемость. Команда, которая тратит 10% времени на осмысление структуры, экономит 50% времени на исправлении последствий спонтанных решений.
Принципы компонентной архитектуры
Когда проект выходит за пределы одного модуля или репозитория, возникает необходимость в компонентах — независимо выпускаемых, повторно используемых единицах кода: NuGet-пакеты, npm-модули, JAR-библиотеки, Docker-образы с чётким API. Компонент — это не просто папка с кодом. Это договорённость о границах ответственности, интерфейсе и жизненном цикле.
Роберт Мартин в «Чистой архитектуре» выделил три принципа, определяющих, как следует формировать такие компоненты. Они — аналог SOLID, но применительно к уровням выше классов: к пакетам, модулям, библиотекам.
1. REP — Reuse / Release Equivalence Principle
(Принцип эквивалентности повторного использования и выпуска)
Формулировка: Классы и модули, предназначенные для повторного использования, должны группироваться в компоненты, которые выпускаются и управляются как единое целое.
Другими словами: если вы хотите использовать некоторую функциональность в нескольких проектах, её нельзя просто копировать файлами. Она должна быть оформлена как отдельный компонент с версионированием, документированным API и процессом релиза.
Почему это важно:
-
Согласованность версий. Если модуль
PaymentCoreиспользуется в трёх сервисах, и в нём исправлен баг, все три сервиса должны обновиться до одной и той же версии. Без пакетного управления легко возникнет ситуация: сервис A использует v1.2, сервис B — v1.3, сервис C — форкнутую копию. Это ведёт к расхождению поведения и трудноуловимым ошибкам. -
Ясность контракта. У компонента есть публичный API и внутренняя реализация. Изменение публичного API требует смены мажорной версии (согласно SemVer). Это заставляет задумываться: действительно ли нужно менять интерфейс? или можно расширить его совместимым образом?
-
Ответственность за качество. Выпуск компонента — это обязательство. Перед релизом должны быть: тесты, документация, проверка обратной совместимости. Это формирует культуру.
REP говорит: если вы планируете переиспользовать — делайте это правильно. В противном случае — оставайтесь в рамках одного приложения, где управление зависимостями проще.
2. CCP — Common Closure Principle
(Принцип общей закрытости)
Формулировка: Классы, которые меняются по одним и тем же причинам и в одно и то же время, должны находиться в одном компоненте.
Это — масштабирование SRP (Single Responsibility Principle) на уровень компонентов. SRP говорит: у класса одна причина для изменения. CCP говорит: у компонента — один мотив для перекомпиляции и релиза.
Пример. Рассмотрим систему с модулями:
OrderValidation— проверка корректности заказа;TaxCalculation— расчёт налогов;InvoiceGeneration— формирование счёта;PdfRenderer— генерация PDF.
Можно сгруппировать так:
- Компонент
Billing:TaxCalculation,InvoiceGeneration,PdfRenderer - Компонент
Orders:OrderValidation
Почему? Потому что налоговые правила, формат счёта и требования к PDF часто меняются одновременно — из-за изменений в законодательстве. Размещение их в одном компоненте означает, что обновление налоговой ставки требует одного релиза, а не трёх. При этом валидация заказа зависит от бизнес-требований к корзине и может меняться независимо — например, при введении новых типов доставки.
CCP помогает избежать двух крайностей:
- Чрезмерной дробности — по одному классу на компонент. Тогда любое изменение требует обновления десятков пакетов, что неэффективно.
- Монолитных компонентов — «всё в одном». Тогда даже мелкое изменение в логике валидации требует пересборки и релиза модуля, отвечающего за генерацию отчётов.
Ключевой вопрос при применении CCP: кто инициирует изменение? Если изменения инициируются одной командой (например, финансовой), то логика, которой владеет эта команда, должна быть в одном компоненте — даже если технически она разнородна.
3. CRP — Common Reuse Principle
(Принцип общей повторяемости)
Формулировка: Классы, не предназначенные для совместного использования, не должны находиться в одном компоненте.
Это — аналог ISP (Interface Segregation Principle), но на уровне компонентов. Если часть компонента используется, а другая — нет, то пользователь вынужден тащить «мёртвый груз», создавая избыточные зависимости.
Пример. Допустим, есть компонент Utils, содержащий:
StringHelper— утилиты для работы со строками;EncryptionService— шифрование данных;LoggingDecorator— декоратор для логирования.
Сервису, которому нужна только StringHelper, всё равно придётся ссылаться на Utils. Если в EncryptionService появится зависимость от внешней библиотеки (например, Bouncy Castle), то все потребители Utils унаследуют эту зависимость — даже если они никогда не шифруют.
CRP требует: если часть функционала используется независимо — выносите её в отдельный компонент.
StringUtilsCryptoCoreTelemetry
Это снижает связанность: сервисы теперь зависят только от того, что им действительно нужно. Но есть цена: больше компонентов — больше накладных расходов на управление. Поэтому CRP применяется селективно: там, где зависимость критична (например, безопасность, лицензирование), или где повторное использование частично, но массово.
Баланс между CCP и CRP
REP, CCP и CRP не всегда совместимы. CCP толкает к объединению, CRP — к разъединению. Выбор — это компромисс, формализуемый через коэффициент устойчивости (Stability Metric), где есть число входящих зависимостей (сколько компонентов зависит от данного) и число исходящих зависимостей (от скольких компонентов зависит данный). Компонент нестабилен — ни от кого не зависит, но и никто не зависит от него (листья дерева), а компонент устойчив — на него много зависимостей, но он почти ни от кого не зависит (ядра, фреймворки).
Устойчивые компоненты (например, DomainModel, CommonTypes) должны быть спроектированы так, чтобы их интерфейсы менялись редко — иначе обновление повлечёт каскад изменений. Нестабильные (например, Adapters.Http, UI.Web) могут меняться часто — они «поглощают» нестабильность окружения.
Оптимальная структура — это направленный ациклический граф зависимостей, где устойчивые компоненты находятся внизу, нестабильные — наверху, и зависимости направлены сверху вниз. Циклов быть не должно — они означают, что границы проведены некорректно.
Связь с DDD
Принципы компонентной архитектуры естественно ложатся на концепцию ограниченных контекстов (Bounded Contexts) из Domain-Driven Design:
- Один ограниченный контекст — один компонент (или группа тесно связанных компонентов по CCP);
- Публичный API контекста — порт (по гексагональной архитектуре);
- Антикоррупционный слой (ACL) — адаптеры, реализующие интеграцию с другими контекстами;
- Совместное использование типов между контекстами — нарушение CRP, если эти типы не являются действительно общими (например,
Money,DateTimeRange).
В такой модели компонент — это граница семантической целостности. Внутри контекста термины имеют однозначный смысл; на стыке — требуется явное преобразование.
Декомпозиция монолита
Декомпозиция — это средство достижения целей: ускорение разработки, повышение отказоустойчивости, возможность независимого масштабирования, снижение времени развёртывания, сокращение инцидентов. Поэтому первое, что следует сделать перед началом — чётко сформулировать метрику успеха: что должно улучшиться и на сколько? Без этого декомпозиция превращается в техническое упражнение с неопределённой отдачей.
Этап 0. Анализ и картирование
Декомпозиция начинается с понимания текущей структуры — особенно если монолит развивался без чётких границ.
Техники анализа:
-
Статический анализ зависимостей. Инструменты (NDepend, Structure101, JDepend, SonarQube) строят граф вызовов между модулями, классами, пакетами. Выявляются:
- циклические зависимости — признак смешанных зон ответственности;
- модули с высокой входящей связанностью (много кто зависит от них) — кандидаты в ядро;
- модули с высокой исходящей связанностью (зависят от многих) — «клей», который нужно изолировать.
-
Анализ по данным. Постройте heat-map обращений к таблицам БД: какие таблицы часто читаются/пишутся вместе? Какие запросы объединяют данные из разных логических областей? Это помогает выявить неявные границы домена — даже если код смешан.
-
Анализ по логам и метрикам. Где происходят пиковые нагрузки? Какие эндпоинты дают наибольшее количество ошибок? Какие компоненты наиболее часто меняются? Это указывает на «горячие точки», где выделение в отдельный сервис даст максимальный эффект.
-
Анализ по истории Git. Какие файлы чаще всего меняются вместе? Можно использовать
git log --follow --name-onlyи построить матрицу коизменяемости. Файлы, которые почти всегда коммитятся в одном MR — кандидаты на один компонент (CCP в действии).
Результат — архитектурная карта: визуализация связей, выделение потенциальных границ, ранжирование по приоритету (влияние / стоимость).
Этап 1. Выбор стратегии
Не все части монолита одинаково подходят для выделения. Существует несколько стратегий, каждая — с разными рисками и выгодами.
1. По функциональной независимости (Strangler Fig Pattern)
Суть: постепенно «оборачивать» функционал монолита новыми компонентами, перехватывая трафик через шлюз (API Gateway). Например:
- Добавляется шлюз перед монолитом.
- Для нового функционала (например, «подписки») создаётся отдельный сервис.
- Шлюз направляет
/api/subscriptions/*в новый сервис, всё остальное — в монолит. - Постепенно переносятся сценарии из монолита в сервис (например, «отмена подписки»).
- Когда весь функционал перенесён — монолитный код удаляется.
Преимущества: минимальный риск — если новый сервис падает, шлюз может вернуть трафик в монолит (fallback). Можно развивать параллельно.
Недостатки: требуется шлюз; возможны проблемы с согласованностью данных (сервис и монолит могут работать с одной БД); сложность при shared-логике (например, проверка прав доступа).
2. По доменным контекстам (DDD-подход)
Выделяются ограниченные контексты — логически целостные области с собственной терминологией и правилами. Например:
- Контекст
Customer Management: профиль, аутентификация, роли. - Контекст
Order Fulfillment: заказы, инвентарь, доставка. - Контекст
Billing: платежи, выставление счётов, возвраты.
Для каждого контекста:
- выделяется подмножество классов и таблиц;
- строится антикоррупционный слой (ACL) — адаптеры для взаимодействия с другими контекстами;
- постепенно мигрируются данные и логика.
Преимущества: высокая семантическая целостность; минимизация межконтекстных зависимостей.
Недостатки: требует глубокого понимания домена; сложность при legacy-коде, где границы размыты.
3. По техническим характеристикам
Выделяются компоненты, чьи требования к инфраструктуре резко отличаются:
- Вычислительно тяжёлые задачи (расчёт кредитного скоринга, обработка изображений) — выносятся в отдельные сервисы с GPU или HPC-оптимизацией.
- Высокочастотные API (публичные эндпоинты) — выносятся в лёгкий фронт-сервис (например, на Go или Rust), снижая нагрузку на основной монолит.
- Асинхронные процессы (отправка email, генерация отчётов) — переводятся на событийную модель с очередями.
Эта стратегия даёт быстрый эффект по производительности и отказоустойчивости.
Этап 2. Управление данными
Наиболее частая ошибка — выделить сервис, но оставить ему прямой доступ к той же базе данных, что и у монолита. Это иллюзия: связанность сохраняется на уровне схемы, и любое изменение таблицы может сломать оба компонента.
Корректные подходы:
1. Собственная БД на первом этапе (Shared Database → Owned Schema)
- Шаг 1: Новый сервис получает доступ к отдельной схеме в той же БД (например,
subscriptions_schema), данные дублируются через триггеры или ETL. - Шаг 2: Постепенно мигрируется логика записи — сначала чтение из новой схемы, потом запись.
- Шаг 3: Отключается доступ к старой схеме; данные конвертируются окончательно.
Плюс: минимизирует простои.
Минус: требует двойной записи (dual-write) и механизмов разрешения конфликтов.
2. Событийная синхронизация (Event Sourcing / CDC)
- Изменения в монолитной БД фиксируются как события (через лог транзакций — Change Data Capture, или через доменные события в коде).
- Новый сервис подписывается на эти события и обновляет свою БД.
- Когда синхронизация стабильна — переключается трафик.
Этот подход обеспечивает eventual consistency, но требует идемпотентных обработчиков и инфраструктуры трассировки.
3. Антикоррупционный слой (ACL)
Если прямой доступ к данным невозможен (например, внешняя система), строится ACL — компонент, инкапсулирующий всю логику взаимодействия:
- преобразование типов (монолитный
DateTime→ ISO-строка); - повторные попытки, таймауты;
- кэширование;
- адаптация к семантике («статус 3 в монолите» →
OrderStatus.Shipped).
ACL защищает новый сервис от нестабильности и несогласованности внешнего API.
Этап 3. Управление транзакциями в распределённой среде
В монолите транзакция BEGIN → INSERT → UPDATE → COMMIT работает «из коробки». В распределённой системе — нет. Варианты:
-
Саги (Saga Pattern) — разбиение транзакции на последовательность локальных транзакций с компенсирующими действиями. Например:
ReserveInventory— резервируем товар.ChargePayment— списываем деньги.- Если шаг 2 падает →
CancelReservation.
Реализуется через orchestration (центральный оркестратор) или choreography (события).
-
Двухфазный коммит (2PC) — возможен в рамках одной СУБД (например, распределённые транзакции в MS DTC), но не масштабируется и не поддерживается в большинстве NoSQL и облачных БД.
-
Идемпотентность — проектирование операций так, чтобы повторный вызов не менял результат (
PUT /orders/{id}с полным состоянием, а неPATCHс инкрементом).
Выбор зависит от требований к консистентности: для банков — саги с компенсацией; для соцсетей — eventual consistency допустима.
Этап 4. Эволюция
Декомпозиция — это многолетний процесс. Успешные примеры (например, Netflix, Amazon) заняли годы. Важно:
- Начинать с малого — выделить один сервис, отработать процессы (CI/CD, мониторинг, отладка), затем масштабировать подход.
- Измерять эффект — до и после: время развёртывания, MTTR, количество инцидентов, скорость внедрения фич.
- Не стремиться к «чистым» микросервисам — гибридные архитектуры (микросервисы + монолитные модули) — норма на промежуточных этапах.
- Инвестировать в инфраструктуру — без централизованного логирования, трассировки, health-check’ов декомпозиция обернётся ростом сложности без выгод.
Инфраструктура как архитектурный фактор
Инфраструктура — это не просто серверы, сети и диски. Это множество решений, инкапсулированных в сервисы, каждое из которых накладывает ограничения и открывает возможности. Архитектор, игнорирующий инфраструктурный контекст, проектирует «в вакууме» — и его решения неизбежно сталкиваются с реальностью при развёртывании.
От статичной среды к управляемым сервисам
Раньше инфраструктура была статичной: арендованный сервер, установленная ОС, настроенная СУБД. Сегодня — динамическая, программно-определяемая, облачная. Ключевое изменение: от ответственности за «железо» к ответственности за контракты сервисов.
Примеры:
- Хранение: выбор между S3 (immutable объекты, eventual consistency), EBS (блочные устройства, strong consistency), RDS (управляемая БД), Aurora (репликация на уровне storage) — определяет, какие паттерны работы с данными допустимы.
- Сеть: VPC, security groups, network ACLs, service mesh (Istio) — задают границы зон доверия, стратегии шифрования (in-transit), ограничения на прямые вызовы.
- Вычисления: EC2 (полный контроль), ECS/EKS (оркестрация контейнеров), Lambda (serverless) — влияют на модель жизненного цикла, управление состоянием, холодные старты.
- Observability: CloudWatch, Prometheus+Grafana, OpenTelemetry — определяют, какие метрики, логи, трассировки доступны «из коробки», и как легко строить диагностические сценарии.
Архитектурное решение «использовать event-driven подход» бессмысленно, если инфраструктура не предоставляет надёжных очередей (Kafka, SQS, Pub/Sub) с гарантиями доставки. Решение «делать микросервисы» рискованно без service discovery и circuit breaker’ов на уровне инфраструктуры.
Зоны и среды
Развёртывание — это не техническая деталь. Это часть архитектуры, влияющая на безопасность, отказоустойчивость, стоимость.
Зоны доступности (Availability Zones)
Развёртывание в нескольких AZ — требование для высокой доступности. Но это требует:
- stateless-приложений (состояние — в БД или кэше с репликацией);
- распределённых БД с синхронной репликацией между AZ;
- балансировщика, поддерживающего failover между зонами.
Если архитектура предполагает shared-состояние в памяти (например, in-memory кэш без инвалидации), то многозоновое развёртывание невозможно без переработки.
Окружения (Environments)
Разделение на dev, test, staging, prod — стандарт. Но архитектурно важно:
- Изоляция данных: одни и те же учётные записи не должны работать в
stagingиprod; тестовые данные — не попадать в продакшен. - Конфигурация как код: различия между окружениями — только через параметры (например, через ConfigMaps в Kubernetes), а не через разный код.
- Синхронизация схемы БД: миграции должны быть идемпотентными и применяться в определённом порядке — иначе staging не отражает состояние prod.
Нарушение этих принципов приводит к «работает у меня, не работает на проде» — как к системному риску.
Multi-region и geo-replication
Для глобальных систем выбор региона — архитектурное решение:
- Latency-based routing (CloudFront, Route53) требует stateless-фронтенда;
- Active-active репликация БД — сложна из-за конфликтов (например, два пользователя одновременно редактируют профиль);
- Active-passive — проще, но требует механизма failover и проверки целостности при переключении.
Здесь архитектура должна учитывать технические ограничения и правовые (GDPR, ФЗ-152): данные граждан РФ должны храниться в РФ — это граница архитектурной модели.
Инфраструктурные компромиссы
Многие инфраструктурные сервисы маскируют сложность за простым API. Но «простота» имеет цену — в виде ограничений.
Пример 1. Serverless (AWS Lambda)
Преимущества: масштабирование «до нуля», оплата за использование, отсутствие управления серверами.
Ограничения:
- Время выполнения (макс. 15 мин) — исключает длительные batch-процессы;
- Холодный старт — неприемлем для low-latency API;
- Ограниченный доступ к сети — нет постоянных TCP-соединений;
- Состояние — только через внешние хранилища (DynamoDB, S3), что увеличивает latency и стоимость.
Архитектура под Lambda требует:
- коротких, идемпотентных функций;
- асинхронной обработки через очереди;
- кэширования на уровне API Gateway или CloudFront.
Попытка запустить монолитное приложение через Lambda — путь к разочарованию.
Пример 2. Managed-БД (RDS, Cloud SQL)
Преимущества: автоматические бэкапы, патчинг, мониторинг.
Ограничения:
- Отсутствие доступа к ОС — нельзя настроить
pg_stat_statementsвручную или оптимизировать ядро; - Ограниченная кастомизация — нельзя поставить свой плагин или изменить параметры, недоступные через консоль;
- Vendor lock-in — экспорт данных может быть медленным и дорогим.
Если приложению критична низкоуровневая оптимизация (например, геопространственные запросы в PostGIS), managed-БД может стать бутылочным горлышком.
Пример 3. Service Mesh (Istio, Linkerd)
Преимущества: централизованное управление трафиком, mTLS «из коробки», retry/circuit breaker без кода.
Сложности:
- Дополнительная задержка (1–3 мс на вызов через sidecar);
- Сложность отладки — трафик «исчезает» в прокси;
- Требования к ресурсам — sidecar потребляет CPU и память.
Service mesh оправдан при десятках сервисов, но избыточен для трёх.
Проектирование «инфраструктуро-осознанно»
Это означает:
-
На раннем этапе определить инфраструктурные ограничения заказчика:
— Облако или on-premise?
— Какие сервисы разрешены?
— Есть ли политики безопасности (шифрование, аудит, изоляция)?
Без этого проектирование идёт вслепую. -
Включать инфраструктурщиков в архитектурные обсуждения — на этапе «как устроить». Например, решение использовать gRPC влияет на выбор балансировщика (не все поддерживают HTTP/2).
-
Моделировать failure modes инфраструктуры:
— Что будет, если сеть между AZ порвётся?
— Что, если очередь переполнится?
— Что, если диск заполнится на 100%?
Архитектура должна предусматривать graceful degradation. -
Документировать инфраструктурные зависимости в ADR:
Решение: использовать DynamoDB вместо PostgreSQL.
Контекст: требование 10K RPS на запись, eventual consistency допустима.
Последствия: отказ от сложных JOIN’ов, необходимость denormalization, обучение команды новой модели данных. -
Использовать Infrastructure as Code (IaC) как спецификацию архитектуры: Terraform-файлы — это формальное описание связей между компонентами («этот сервис зависит от этой очереди и этого кластера БД»).